﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections;

namespace DisableEvent
{
    internal class ResubscribeInfo
    {
        public EventInfo Evnt { get; private set; }
        public object EventSource { get; private set; }
        public Delegate ToBeResubscribed { get; private set; }

        public ResubscribeInfo(EventInfo evnt, object eventSource, Delegate toBeResubscribed)
        {
            Evnt = evnt;
            EventSource = eventSource;
            ToBeResubscribed = toBeResubscribed;
        }
    }

    public class DisableEvent : IDisposable
    {
        private static BindingFlags _privatePublicStaticInstance =
            BindingFlags.IgnoreCase |
            BindingFlags.NonPublic |
            BindingFlags.Public |
            BindingFlags.Instance |
            BindingFlags.Static |
            BindingFlags.FlattenHierarchy;

        private List<ResubscribeInfo> _resubscribeInfo = new List<ResubscribeInfo>();

        public DisableEvent(object eventSource, object eventSubscriber, string handlerName)
        {
            if (eventSource == null || eventSubscriber == null ||
                       string.IsNullOrEmpty(handlerName))
                throw new ArgumentNullException();

            var tSource = eventSource.GetType();
            var tSubscriber = eventSubscriber.GetType();

            //

            MethodInfo targetMethod = GetMethod(tSubscriber, handlerName);
            if (targetMethod == null)
                throw new InvalidOperationException("Method " + handlerName + " was not found");

            //

            if (!InternalDisable(eventSource, eventSubscriber, targetMethod, tSource))
            {
                // if it is enumerable, disable event on all elements
                if (eventSource is IEnumerable)
                {
                    tSource = null;

                    foreach (var el in eventSource as IEnumerable)
                    {
                        if (tSource == null) tSource = el.GetType();

                        InternalDisable(el, eventSubscriber, targetMethod, tSource);
                    }
                }
            }
        }

        public void Dispose()
        {
            if (_resubscribeInfo != null)
            {
                foreach (var resub in _resubscribeInfo)
                {
                    resub.Evnt.GetAddMethod(true).Invoke(
                      resub.EventSource, new object[] { resub.ToBeResubscribed });
                }
            }

            _resubscribeInfo = null;
        }

        private bool InternalDisable(object eventSource,
          object eventSubscriber, MethodInfo targetMethod, Type tSource)
        {
            bool found = false;

            foreach (var e in tSource.GetEvents())
            {
                var field = GetField(tSource, e.Name);
                var deleg = (Delegate)field.GetValue(eventSource);

                if (deleg != null)
                {
                    var invocList = deleg.GetInvocationList();

                    var toBeUnsubscribed = invocList.FirstOrDefault(
                      x => x.Target == eventSubscriber && x.Method == targetMethod);

                    if (toBeUnsubscribed != null)
                    {
                        e.GetRemoveMethod(true).Invoke(eventSource, new object[] { toBeUnsubscribed });
                        _resubscribeInfo.Add(new ResubscribeInfo(e, eventSource, toBeUnsubscribed));

                        found = true;
                        break;
                    }
                }
            }

            return found;
        }

        private FieldInfo GetField(Type type, string name)
        {
            if (type.IsInterface)
            {
                var considered = new List<Type>();
                var queue = new Queue<Type>();
                considered.Add(type);
                queue.Enqueue(type);
                while (queue.Count > 0)
                {
                    var subType = queue.Dequeue();
                    foreach (var subInterface in subType.GetInterfaces())
                    {
                        if (considered.Contains(subInterface)) continue;

                        considered.Add(subInterface);
                        queue.Enqueue(subInterface);
                    }

                    var prop = subType.GetField(name, _privatePublicStaticInstance);
                    if (prop != null) return prop;
                }
            }

            var retval = type.GetField(name, _privatePublicStaticInstance);
            if (retval == null)
            {
                var btype = type.BaseType;
                if (btype != null) retval = GetField(btype, name);
            }

            return retval;
        }
        private MethodInfo GetMethod(Type type, string name)
        {
            while (type != null)
            {
                var method = type.GetMethod(name, _privatePublicStaticInstance);
                if (method != null) return method;
                type = type.BaseType;
            }

            return null;
        }
    }
}
